fix: remove unsound structured-output Tapioca compiler - #405
Conversation
OkTest Summary🟡 Running for Ruby SDK PR #405. SDK merge (pending) · head ( |
HAYDEN-OAI
left a comment
There was a problem hiding this comment.
Reviewed the compiler rollback, transport-model compatibility, structured-output hydration, and optional Sorbet boundary. Looks good.
HAYDEN-OAI
left a comment
There was a problem hiding this comment.
Approved after reviewing all six changed files and the coordinated Castiron generator removal. The unreleased compiler can be removed without breaking published APIs; direct model construction and assignment still preserve caller-owned raw values, while both Chat Completions and Responses hydrate nested structured outputs. Nullable-array Sorbet types, optional sorbet-runtime, and Rails/Tapioca loader behavior remain intact.
Automated Release PR --- ## [0.79.0](openai/openai-ruby@v0.78.0...v0.79.0) (2026-08-14) ### Features * add HTTP response observability ([openai#365](openai#365)) ([48d36b7](openai@48d36b7)) * add Tapioca typing for structured outputs ([openai#364](openai#364)) ([738e2b4](openai@738e2b4)) * **api:** Add new model identifiers and remove audit log source ([e0a4bc5](openai@e0a4bc5)) * **api:** add WebSocket stream IDs ([openai#387](openai#387)) ([5880287](openai@5880287)) * **api:** add workload identity access token issued event ([openai#372](openai#372)) ([9199779](openai@9199779)) * **api:** deprecate Sora video APIs ([openai#386](openai#386)) ([0664456](openai@0664456)) * **client:** add default headers ([openai#369](openai#369)) ([d516874](openai@d516874)) * expose request IDs ([openai#352](openai#352)) ([c15fb7b](openai@c15fb7b)) * support Azure OpenAI v1 ([openai#355](openai#355)) ([c1d223e](openai@c1d223e)) ### Bug Fixes * **api:** Add new model identifiers and remove audit log source ([openai#360](openai#360)) ([e0a4bc5](openai@e0a4bc5)) * **api:** allow nil timeout in client signatures ([openai#363](openai#363)) ([e52ca61](openai@e52ca61)) * **api:** document file upload metadata defaults ([openai#361](openai#361)) ([b4bc1ea](openai@b4bc1ea)) * coerce nested BaseModel fields ([openai#295](openai#295)) ([e5152a9](openai@e5152a9)) * enable streaming when retrieving responses ([openai#413](openai#413)) ([fd85182](openai@fd85182)) * encode multipart array and nested fields ([openai#348](openai#348)) ([1c71a1e](openai@1c71a1e)) * honor workload identity environment defaults ([openai#398](openai#398)) ([f4afea1](openai@f4afea1)) * keep required path parameters out of resource query strings ([openai#402](openai#402)) ([6768a7c](openai@6768a7c)) * loading after ActiveSupport 6 subclass extensions ([openai#346](openai#346)) ([20fbb09](openai@20fbb09)) * make SDK debug body logging fail closed ([openai#411](openai#411)) ([c4f7284](openai@c4f7284)) * package every README-linked guide and example ([openai#382](openai#382)) ([ddecd0f](openai@ddecd0f)) * preserve binary multipart stream reads ([openai#354](openai#354)) ([b3a1ba7](openai@b3a1ba7)) * preserve existing BaseModel instances during coercion ([openai#400](openai#400)) ([fbd8560](openai@fbd8560)) * preserve prefixed idempotency headers on redirects ([openai#404](openai#404)) ([c380e13](openai@c380e13)) * preserve stream identity with HTTP logging ([openai#384](openai#384)) ([640a1fe](openai@640a1fe)) * prevent symbol-keyed headers from bypassing security filters ([openai#383](openai#383)) ([b8165af](openai@b8165af)) * redact sensitive query and form logging ([openai#389](openai#389)) ([7aa54e4](openai@7aa54e4)) * reject invalid webhook signing secrets ([openai#403](openai#403)) ([ad12458](openai@ad12458)) * remove unsound structured-output Tapioca compiler ([openai#405](openai#405)) ([826769a](openai@826769a)) * restore RuboCop coverage for RBI files ([openai#388](openai#388)) ([d2d4b5d](openai@d2d4b5d)) * return values from interruptible enumerator ([openai#350](openai#350)) ([327dad7](openai@327dad7)) * strip credential headers on cross-origin redirects ([openai#391](openai#391)) ([f50f08c](openai@f50f08c)) * tighten JSON, JSONL, and SSE content type matching ([openai#277](openai#277)) ([66359a0](openai@66359a0)) * use API field names in structured output schemas ([openai#390](openai#390)) ([d1e3cf3](openai@d1e3cf3)) * validate and bound retry delays ([openai#392](openai#392)) ([fff94e7](openai@fff94e7)) * **webhooks:** support Rack and case-insensitive HTTP headers ([openai#401](openai#401)) ([37de980](openai@37de980)) ### Reverts * nested BaseModel coercion ([openai#295](openai#295)) ([openai#375](openai#375)) ([65fa76e](openai@65fa76e)) ### Chores * Cover Ruby files at the SDK root ([openai#362](openai#362)) ([d11f25b](openai@d11f25b)) * enforce boolean symbol lint ([openai#395](openai#395)) ([fafdf2e](openai@fafdf2e)) * enforce deprecated constant lint ([openai#379](openai#379)) ([0def912](openai@0def912)) * enforce duplicate match pattern lint ([openai#380](openai#380)) ([7a8f2fb](openai@7a8f2fb)) * enforce empty else lint ([openai#396](openai#396)) ([1e073c1](openai@1e073c1)) * enforce line length lint ([openai#406](openai#406)) ([4592061](openai@4592061)) * enforce line length on base models ([openai#416](openai#416)) ([aef1e33](openai@aef1e33)) * enforce line length on requires ([openai#415](openai#415)) ([c24e045](openai@c24e045)) * enforce missing RuboCop enable directives ([openai#368](openai#368)) ([50046bf](openai@50046bf)) * enforce missing super lint ([openai#393](openai#393)) ([2a42bd0](openai@2a42bd0)) * enforce nonempty pattern branches ([openai#409](openai#409)) ([9d22d5c](openai@9d22d5c)) * enforce redundant directive lint ([openai#366](openai#366)) ([8448a4c](openai@8448a4c)) * enforce redundant exception lint ([openai#397](openai#397)) ([cbda8bc](openai@cbda8bc)) * enforce symbol conversion lint ([openai#381](openai#381)) ([0531498](openai@0531498)) * enforce useless assignment lint ([openai#394](openai#394)) ([9688836](openai@9688836)) * guard RuboCop suppression directives ([openai#408](openai#408)) ([5b6e0dc](openai@5b6e0dc)) * lint Ruby files repository-wide ([openai#377](openai#377)) ([8ca8426](openai@8ca8426)) * remove Stainless attribution and infrastructure ([openai#371](openai#371)) ([33d2c86](openai@33d2c86)) * require MFA for gem releases ([openai#378](openai#378)) ([71798ec](openai@71798ec)) ### Documentation * **api:** describe response stream event unions ([openai#412](openai#412)) ([87d18ef](openai@87d18ef)) * clarify file upload metadata ([openai#358](openai#358)) ([698ac65](openai@698ac65)), closes [openai#243](openai#243) * document fiber scheduler concurrency ([openai#357](openai#357)) ([25a7bbc](openai@25a7bbc)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: openai-sdks[bot] <284451331+openai-sdks[bot]@users.noreply.github.com>
Summary
OpenAI::BaseModelTapioca DSL compiler, its compiler-specific README section, and the compiler-only tests.sorbet-runtimeremains optional for normal SDK usage.Why this is the correct architectural rollback
#364 addressed the customer report in #309 by adding a compiler that generated readers such as
ParticipantandT::Array[Participant]. Those signatures are unsound for the SDK's supported transport-model contract:Parsed API responses correctly hydrate those same fields into application-defined model objects. The original compiler test only type-checked synthetic RBI usage; it never compared generated reader signatures with direct model construction, assignment, or both public parsed-response boundaries. The proposed runtime workaround in #385 introduced additional coercion and conflicting accessor/storage semantics instead of addressing the incorrect abstraction.
Architecture review concluded that SDK transport models should remain permissive transport models. Sound application-owned typed structured outputs should instead be designed at the structured-output integration boundary. #309 has therefore been reopened and rewritten around the original customer outcome rather than the rejected implementation.
The removed compiler has not appeared in a released gem, so removing it now avoids publishing an unsound API contract. The actual live Castiron generator also emitted this compiler; its coordinated upstream removal is https://github.com/openai/openai/pull/1291001.
Why retain the other #364 changes
lib/openai.rbloader detection independently fixes Rails/Tapioca load order and retains its existing regression coverage.ArrayOf#to_sorbet_typecorrectly preserves nullable array elements independently of the deleted compiler; the new isolated subprocess test preserves coverage without making Sorbet a runtime dependency.OpenAI::BaseModeltests cover both individual nested fields and nested arrays.Validation
bundle exec rake lint: 2,618 RuboCop files clean, Sorbet clean, 1,212 RBS files valid.bundle exec rake build:gem; inspected the built gem to verify it contains no Tapioca compiler and does not addsorbet-runtimeas a runtime dependency.Related customer issue: #309. Do not auto-close it: the underlying typed structured-output design remains open.